#pragma once
#include <glad/glad.h>
#include <glm/gtc/type_ptr.hpp>
#include <any>
#include <typeinfo>
#include "core/cbase.h"
#include "core/CColor.h"
#include "Enum.h"

static GLuint CurrentUsingProgram = InvalidNum;
static CString CurrentUsingProgramShaderName;// just for locate the wrong location when error occur

static const UInt ZhicheTexUnit = 1;
static const CString ZhicheSampler2DName = "zhicheTex";

struct Sampler2DArg {
	GLuint texture;
	UInt texUnit;
	std::string name;

	Sampler2DArg() = default;
	Sampler2DArg(GLuint texture, UInt texUnit, const CString& name) : texture(texture), texUnit(texUnit), name(name) {}

	bool isValid() const { return !name.empty(); };
};

#define Field(K,V) MakePair(K,V)

/**
* @note Once you set the value of a uniform variable through the glUniform * function, this value will remain until you call glUniform * again to update it, or until the program object is deleted!
*/
class Uniform {
public:
	Uniform(GLuint ID = InvalidNum) { this->ID = ID; }

	bool isCreated() { return this->ID != InvalidNum; }

	bool isValid() {
		ASSERT(CurrentUsingProgram == this->ID, "current program is not used currently!");
		return true;
	}

	void add(const CString& name, const std::any& value) {
		ASSERT(isValid(), "current shader program is not used!");
		auto key = name.c_str();
		auto& type = value.type();
		try {
			if (type == typeid(bool)) setBool(key, std::any_cast<bool>(value));
			else if (type == typeid(int)) setInt(key, std::any_cast<int>(value));
			else if (type == typeid(double)) setFloat(key, std::any_cast<double>(value));
			else if (type == typeid(float)) setFloat(key, std::any_cast<float>(value));
			else if (type == typeid(CColor)) setColor(key, std::any_cast<CColor>(value));
			else if (type == typeid(P)) setP2D(key, std::any_cast<P>(value).toVec2());
			else if (type == typeid(Vec2)) setP2D(key, std::any_cast<Vec2>(value));
			else if (type == typeid(Vec3)) setP3D(key, std::any_cast<Vec3>(value));
			else if (type == typeid(Vec4)) setP4D(key, std::any_cast<Vec4>(value));
			else if (type == typeid(Mat)) setMat(key, std::any_cast<Mat>(value));
		} catch (const std::bad_any_cast& e) {
			LogEE << "failed to cast: " << e.what();
		}
	}

	/**@example addStruct("material", Field("baseColor", myBaseColor), Field(k2,v2), ...);*/
	void addStruct(const std::string&) {}// a must: stop condition
	template<typename T, typename... Args>
	void addStruct(const std::string& structName, const Pair<const char*, T>& field, Args... args) {
		add(structName + "." + field.key, field.value);
		if (sizeof...(args) > 0)
			addStruct(structName, args...);
	}

	template<typename T>
	void addArray(const CString& name, const CVector<T>& values) {
		for (int i = 0; i < values.size(); i++) {
			const auto Name = CString("%1[%2]").arg(name).arg(i);
			const auto Value = values[i];
			add(Name, Value);
		}
	}

	/**
	* @note OpenGL will use GL_TEXTURE0 as the current active texture unit.
	*       If you do not explicitly activate other texture units, all texture operations will be applied to GL_TEXTURE0 by default.
	*	    so, no uniform name needed if visit the current active texture unit, you can use any name to accept the sampler2D texture data.
	*/
	void setSampler2D(const Sampler2DArg& sampler2DArg) {
		if (!sampler2DArg.isValid())
			return;
		glActiveTexture(GL_TEXTURE0 + sampler2DArg.texUnit);
		glBindTexture(GL_TEXTURE_2D, sampler2DArg.texture);
		if (!sampler2DArg.name.empty())
			setInt(sampler2DArg.name.c_str(), sampler2DArg.texUnit);
	}

private:
	GLuint ID;

	// utility uniform functions
	// ------------------------------------------------------------------------
	void setBool(const char* name, bool value) {
		glUniform1i(getUniformLocation(name), (int)value);
	}
	// ------------------------------------------------------------------------
	void setInt(const char* name, int value) {
		glUniform1i(getUniformLocation(name), value);
	}
	// ------------------------------------------------------------------------
	void setFloat(const char* name, float value) {
		glUniform1f(getUniformLocation(name), value);
	}
	// ------------------------------------------------------------------------
	void setP2D(const char* name, const Vec2& vec) {
		glUniform2fv(getUniformLocation(name), 1, PtrOf(vec));
	}
	void setP2D(const char* name, float x, float y) {
		glUniform2f(getUniformLocation(name), x, y);
	}
	// ------------------------------------------------------------------------
	void setP3D(const char* name, const Vec3& vec) {
		glUniform3fv(getUniformLocation(name), 1, PtrOf(vec));
	}
	void setP3D(const char* name, float x, float y, float z) {
		glUniform3f(getUniformLocation(name), x, y, z);
	}
	// ------------------------------------------------------------------------
	void setP4D(const char* name, const Vec4& vec) {
		glUniform4fv(getUniformLocation(name), 1, PtrOf(vec));
	}
	void setP4D(const char* name, float x, float y, float z, float w) {
		glUniform4f(getUniformLocation(name), x, y, z, w);
	}
	void setColor(const char* name, const CColor& color) {
		if (!color.hasAlphaChannel())
			setP3D(name, Vec3(color.redF(), color.greenF(), color.blueF()));
		else
			setP4D(name, Vec4(color.redF(), color.greenF(), color.blueF(), color.alphaF()));
	}
	// ------------------------------------------------------------------------
	void setMat(const char* name, const Mat& mat) {
		glUniformMatrix4fv(getUniformLocation(name), 1, GL_FALSE, PtrOf(mat));
	}
	// ------------------------------------------------------------------------
	void setMat3(const char* name, const Mat3& mat) {
		glUniformMatrix3fv(getUniformLocation(name), 1, GL_FALSE, PtrOf(mat));
	}
	// ------------------------------------------------------------------------
	void setMat2(const char* name, const Mat2& mat) {
		glUniformMatrix2fv(getUniformLocation(name), 1, GL_FALSE, PtrOf(mat));
	}

	/** @note the glGetUniformLocation function needs to be called when a valid shader program is activated in order to correctly retrieve the location of the specified uniform variable! */
	GLint getUniformLocation(const char* name) {
		GLint location = glGetUniformLocation(ID, name);CHECK_GL_ERR_MSG((CString("location name = '") << name << "', shaderName = '" << CurrentUsingProgramShaderName << "'").c_str())
		if (location == -1) {}// location == -1 will be ignored, which basically means: optimized by code made it unused
		//LogW << "uniform location not found! => May:: GLSL unuse it or be optimized by code auto. uniform::name='" << name << "', programID=" << ID << ", shaderName=" << CurrentUsingProgramShaderName;
		return location;
	}
};